%%% ====================================================================
%%% An XML parser, using the SAX model.
%%% 
%%% Copyright (C) 2006 Willem de Jong
%%%
%%% This program is free software; you can redistribute it and/or modify
%%% it under the terms of the GNU General Public License as published by 
%%% the Free Software Foundation; either version 2 of the License, or 
%%% (at your option) any later version.
%%%
%%% This program is distributed in the hope that it will be useful, but
%%% WITHOUT ANY WARRANTY; without even the implied warranty of
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
%%% General Public License for more details.
%%%
%%% You should have received a copy of the GNU General Public License 
%%% along with this program; if not, write to the Free Software 
%%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
%%% USA
%%%
%%% Author contact: w.a.de.jong@gmail.com
%%% ====================================================================

%%% Version: 0.0.2

-module(erlsom_sax).

-include("erlsom_sax.hrl").

-export([parseDocument/3]).

%%%%%%%%%%%%%%%%%%%%%%%%%
%%
%%  Interface
%%
%%  parseDocument(Xml, State, EventFun)
%%
%%  Xml = A list of integers that correspond with the characters in an XML 
%%    document. Can be either 1 byte characters according to ISO ...,
%%    or integers that correspond to Unicode code points (see...).
%%
%%  State - a term() that is passed to the EventFun. 
%%
%%  Eventfun - a fun() that is called by the parser whenever it has parsed 
%%    a bit of the Xml input. The function is called by the parser according
%%    to the Sax specification (see [SAX]).
%%
%%    EventFun should accept the following arguments:
%%    - Event, a tuple that describes the event, see below.
%%    - State - a term()
%%
%%    EventFun should return State, a term() that wil be passed back to the next
%%    invocation of EventFun.
%%
%%  Returns: State 
%%    (i.e.: the result of the last invocation of the callback function)
%%
%%%%%%%%%%%%%%%%%%%%%%%%%

%%%%%%%%%%%%%%%%%%%%%%%%%
%%
%%  Events sent out by the SAX parser.
%%
%%  Based on org.xml.sax ContentHandler interface [SAX].
%%
%%  startDocument
%%
%%  endDocument 
%%    Will NOT be sent out in case of an error
%%
%%  {startPrefixMapping, Prefix, URI}
%%    Begin the scope of a prefix - URI namespace mapping
%%    Will be sent immediately before the corresponding startElement event.
%%
%%  {endPrefixMapping, Prefix}
%%    End the scope of a prefix - URI namespace mapping
%%    Will be sent immediately before the corresponding endElement event.
%% 
%%  {startElement, Uri, LocalName, Prefix, [Attributes]}
%%    Receive notification of the beginning of an element.
%%    There will be a corresponding endElement (even when the element is
%%    empty).
%%    All three name components will be provided.
%%
%%    [Attributes] is a list of attribute records, see sax.hrl.
%%    Namespace attributes (xmlns:*) will not be reported.
%%    There will be NO attribute values for defaulted attributes!
%%
%%    Providing 'Prefix'in stead of 'Qualified name' is probably not quite
%%    in line with the SAX spec, but it appears to be more convenient.
%%
%%  {endElement, Uri, LocalName, Prefix}
%%    Receive notification of the end of an element.
%%
%%  {characters, Characters}
%%    Receive notification  of character data.
%%    All character data will be in one chunk, except if there is a 
%%    CDATA section included inside a character section. In that case
%%    there will be separate events for the characters before the CDATA, the
%%    CDATA section and the characters following it (if any, of course).
%%    
%%  {ignorableWhitespace, Characters}
%%    If a character data section (as it would be reported by the 'characters'
%%    event, see above) consists ONLY of whitespace, it will be 
%%    reported as ignorableWhitespace.
%%
%%  {processingInstruction, Target, Data}
%%
%%  {error, Description}
%%  {internalError, Description}
%%
%%%%%%%%%%%%%%%%%%%%%%%%

%% debug(Text) ->
   %% io:format("~p\n", [Text]).

%% returns State
parseDocument(Xml, State, Callback) ->
  State2 = Callback(startDocument, State),
  case catch parseProlog(Xml, State2, Callback) of
    {error, Message} -> Callback({error, Message}, State2);
    {'EXIT', Message} -> Callback({internalError, Message}, State2);
    State3 -> Callback(endDocument, State3)
  end.

%% returns State
%% TODO: something special for the XML declaration
parseProlog([$<, $? | Tail], State, Callback) ->
  {processinginstruction, Target, Data, Tail2} = 
    parseProcessingInstruction(Tail),
  State2 = Callback({processingInstruction, Target, lists:reverse(Data)}, State),
  parseProlog(Tail2, State2, Callback);

parseProlog([$<, $!, $-, $- | Tail], State, Callback) ->
  {comment, Tail2} = parseComment(Tail),
  parseProlog(Tail2, State, Callback);

parseProlog("<!DOCTYPE" ++ Tail, State, Callback) ->
  {dtd, Tail2} = parseDTD(Tail),
  parseProlog(Tail2, State, Callback);

%% whitespace in the prolog is ignored
parseProlog([Char | Tail], State, Callback) ->
  case typeOfMarkupChar(Char) of
    whitespace ->
      parseProlog(Tail, State, Callback);
    lessthan ->
      {_Tail2, State2} = parseElement([], Tail, State, Callback),
      %% To do: what if not tail == []?
      State2;
    _Else ->
      throw({error, "Malformed: Illegal character in document"})
  end;

parseProlog([], _, _) ->
  throw({error, "Malformed: Unexpected end of text in document"}).

%% returns {cdata, CData, Tail}
parseCDATA(Head, Tail) ->
  case Tail of
    [$], $], $> | Tail2] -> {cdata, lists:reverse(Head), Tail2};
    [] -> throw({error, "Malformed: Unexpected end of text in CData"});
    [NextChar | Tail2] -> parseCDATA([NextChar | Head], Tail2)
  end.

%% returns {dtd, Tail} 
parseDTD([ $[ | Tail]) ->
  {intSubset, Tail2} = parseIntSubset(Tail),
  parseDTD(Tail2);
parseDTD([ $> | Tail]) ->
  {dtd, Tail};
parseDTD([ _NextChar | Tail]) ->
  parseDTD(Tail);
parseDTD([]) ->
  throw({error, "Malformed: Unexpected end of text in DTD"}).

%% returns {intSubset, Tail} 
parseIntSubset([ $] | Tail]) ->
  {intSubset, Tail};
parseIntSubset([ _NextChar | Tail]) ->
  parseIntSubset(Tail);
parseIntSubset([]) ->
  throw({error, "Malformed: Unexpected end of text in DTD"}).


%% returns {comment, Tail}
%% TODO: this accepts CDATA in a comment, is that correct?
parseComment([NextChar | Tail]) ->
  case NextChar of
    $- ->
      case Tail of
        [$-, $> | Tail2] -> {comment, Tail2};
	_ -> parseComment(Tail)
      end;
    $< ->
      case Tail of 
        [$!, $[, $C, $D, $A, $T, $A, $[ | Tail2] ->
	  case parseCDATA([], Tail2) of
	    {cdata, _Cdata, Tail3} -> parseComment(Tail3);
	    Other -> Other
	  end;
	_ -> parseComment(Tail)
      end;
    _ -> parseComment(Tail)
  end;

parseComment([]) ->
  throw({error, "Malformed: Unexpected end of text in comment"}).

%% returns {processinginstruction, Target, Data, Tail}
parseProcessingInstruction(Tail) ->
  {Target, Tail2} = parseNameNoNamespaces(Tail),
  {Data, Tail3} = parsePIData([], Tail2),
  {processinginstruction, Target, Data, Tail3}.

%% returns {Data, Tail}
parsePIData(Head, Tail) ->
  case Tail of
    [$?, $> | Tail2] -> {Head, Tail2};
    [] -> throw({error, "Malformed: Unexpected end of text in processing instruction"});
    [C | Tail2] -> parsePIData([C | Head], Tail2)
  end.

%% parseElement(Namespaces, Tail, State, Callback)
%% Namespaces = a list of namespace tuples {Prefix, Uri}
%% Tail = the XML string that we are parsing
%%
%% returns {Tail, State}, the remaining text in the XML document and the 
%% current State.
%%
parseElement(Namespaces, Tail, State, Callback) ->
  case parseStartTag(Tail) of
    {emptyelement, {Prefix, _LocalName, _QName}=StartTag, Attributes, Tail2} ->
      {{Uri, LocalName, QName}, Attributes2, NewNamespaces} = 
          createStartTagEvent(StartTag, Namespaces, Attributes),
      %% Call the call back functions
      State2 = mapStartPrefixMappingCallback(NewNamespaces, State, Callback),
      State3 = Callback({startElement, Uri, LocalName, Prefix, Attributes2}, State2),
      State4 = Callback({endElement, Uri, LocalName, QName}, State3),
      State5 = mapEndPrefixMappingCallback(NewNamespaces, State4, Callback),

      {Tail2, State5};
    {starttag, {Prefix, _LocalName, QName} = StartTag, Attributes, Tail2} ->
      {{Uri, LocalName, Prefix}, Attributes2, NewNamespaces} = 
          createStartTagEvent(StartTag, Namespaces, Attributes),
      %% Call the call back function
      State2 = mapStartPrefixMappingCallback(NewNamespaces, State, Callback),
      State3 = Callback({startElement, Uri, LocalName, Prefix, Attributes2}, State2),

      %% parse whatever is between the tags
      {Tail3, State4} = parseContent(Namespaces ++ NewNamespaces, Tail2, State3, Callback),
      case parseEndTag(Tail3, []) of
        {endtag, QName, Tail4} -> 
          %% Call the call back functions for the end tag
          State5 = Callback({endElement, Uri, LocalName, Prefix}, State4),
          State6 = mapEndPrefixMappingCallback(NewNamespaces, State5, Callback),

	  {Tail4, State6};
	{endtag, _, _} -> throw({error, "Malformed: Tags don't match"})
      end
  end.
 
%% function to call the Callback function for all elements in a list of 'new namespaces'.
%% returns State
mapStartPrefixMappingCallback([{Prefix, Uri} | Tail], State, Callback) ->
  mapStartPrefixMappingCallback(Tail, Callback({startPrefixMapping, Prefix, Uri}, State), Callback);

mapStartPrefixMappingCallback([], State, _Callback) ->
  State.

%% function to call the Callback function for all elements in a list of 'new namespaces'.
%% returns State
mapEndPrefixMappingCallback([{Prefix, _Uri} | Tail], State, Callback) ->
  mapEndPrefixMappingCallback(Tail, Callback({endPrefixMapping, Prefix}, State), Callback);

mapEndPrefixMappingCallback([], State, _Callback) ->
  State.

%% the '<' is already removed
%% returns {starttag, StartTag, Attributes, Tail}
%% or {emptyelement, StartTag, Attributes, Tail}
%%
%% where StartTag = {Prefix, LocalName, QualifiedName}
%%
parseStartTag(Tail) ->
  {StartTag, Tail2} = parseName(Tail),
  {attributes, Attributes, Tail3} = parseAttributes([], Tail2),
  case Tail3 of
    [$> | Tail4] -> 
      {starttag, StartTag, Attributes, Tail4};
    [$/, $> | Tail4] -> 
      {emptyelement, StartTag, Attributes, Tail4};
    [] ->
      throw({error, "Malformed: Unexpected end of document in start tag"});
    _ ->
      throw({error, "Malformed: Unexpected character: " ++ [hd(Tail3)]})
  end.
      
%% returns {Name, Tail}, where
%% Name = {Prefix, LocalName, QualifiedName}
%%
%% To do: introduce a parameter that indicates whether we are using 
%% namespaces. 
parseName([FirstChar | Tail]) ->
  %% this should differentiate between 'with namespaces'and 'without'
  %% for the moment the assumption is 'with', therfore a name cannot
  %% start with a ':'.
  case typeOfMarkupChar(FirstChar) of
    namestartchar when FirstChar /= $: -> parseName([FirstChar], Tail);
    _ -> throw({error, "Malformed: Illegal character in tag"})
  end;

parseName([]) -> throw({error, "Malformed: Unexpected end of document in name"}).

parseName(Head, T = [NextChar | Tail]) ->
  case typeOfNameChar(NextChar) of
    namechar when NextChar == $: ->
      %% Head is the prefix
      parseName(Head, [], Tail);
    namechar ->
      parseName([NextChar | Head], Tail);
    whitespace -> 
      %% Head is the local name (no prefix)
      {{[], lists:reverse(Head), lists:reverse(Head)}, T};
    morethan ->
      %% Head is the local name (no prefix)
      {{[], lists:reverse(Head), lists:reverse(Head)}, T};
    equalsign -> 
      %% Head is the local name (no prefix)
      %% !! should differentiate between attribute name and tag!?
      {{[], lists:reverse(Head), lists:reverse(Head)}, T};
    slash ->
      %% Head is the local name (no prefix)
      {{[], lists:reverse(Head), lists:reverse(Head)}, T};
    _ ->
      throw({error, "Malformed: Unexpected character in name"})
  end;

parseName(_, []) -> throw({error, "Malformed: Unexpected end of document in name"}).
      
%% should there be another check on the first character of the local name?
parseName(Prefix, Head, T = [NextChar | Tail]) ->
  case typeOfNameChar(NextChar) of
    namechar when NextChar == $: ->
      %% only 1 colon is allowed
      throw({error, "Malformed: Unexpected character in name"});
    namechar ->
      parseName(Prefix, [NextChar | Head], Tail);
    whitespace -> 
      {{lists:reverse(Prefix), lists:reverse(Head), 
        lists:reverse(Head ++ ":" ++ Prefix)}, T};
    morethan ->
      {{lists:reverse(Prefix), lists:reverse(Head), 
        lists:reverse(Head ++ ":" ++ Prefix)}, T};
    equalsign ->
      {{lists:reverse(Prefix), lists:reverse(Head), 
        lists:reverse(Head ++ ":" ++ Prefix)}, T};
    slash ->
      {{lists:reverse(Prefix), lists:reverse(Head), 
        lists:reverse(Head ++ ":" ++ Prefix)}, T};
    _ ->
      throw({error, "Malformed: Unexpected character in name"})
  end;

parseName(_, _, []) -> throw({error, "Malformed: Unexpected end of document in name"}).

%% returns {Name, Tail}
parseNameNoNamespaces([FirstChar | Tail]) ->
  case typeOfMarkupChar(FirstChar) of
    namestartchar -> parseNameNoNamespaces([FirstChar], Tail);
    _ -> throw({error, "Malformed: Illegal character in name: " ++ [FirstChar]})
  end;

parseNameNoNamespaces([]) 
  -> throw({error, "Malformed: Unexpected end of document in name"}).
      
parseNameNoNamespaces(Head, T = [NextChar | Tail]) ->
  case typeOfNameChar(NextChar) of
    whitespace -> 
      {lists:reverse(Head), T};
    morethan ->
      {lists:reverse(Head), T};
    questionmark ->
      {lists:reverse(Head), T};
    equalsign ->
      {lists:reverse(Head), T};
    namechar ->
      parseNameNoNamespaces([NextChar | Head], Tail);
    _ ->
      throw({error, "Malformed: Unexpected character in name"})
  end;

parseNameNoNamespaces(_, []) 
  -> throw({error, "Malformed: Unexpected end of document in name"}).
      
%% returns: {attributes, Attributes, Tail}}
%% Attributes = list of {Name, Value} tuples, and
%% Name = {Prefix, LocalName, QualifiedName}.
parseAttributes(Attributes, T = [NextChar | Tail]) ->
  case typeOfMarkupChar(NextChar) of
    whitespace -> parseAttributes(Attributes, Tail);
    slash -> {attributes, Attributes, T};
    morethan -> {attributes, Attributes, T};
    namestartchar -> 
      {attribute, Attribute, Tail2} = parseAttribute(T),
      parseAttributes([Attribute | Attributes], Tail2);
    _ -> 
      throw({error, "Malformed: Unexpected character in attribute: " ++ [NextChar]})
  end;

parseAttributes(_Attributes, []) ->
  throw({error, "Malformed: Unexpected end of string in attribute"}).

%% returns {attribute, Attribute, Tail} where 
%% Attribute = {Name, Value}, and
%% Name = {Prefix, LocalName}.
parseAttribute(Tail) ->
  {AttributeName, Tail2} = parseName(Tail),
  parseAttributeValue(AttributeName, Tail2).

%% returns {attribute, {Name, Value}, Tail}
parseAttributeValue(AttributeName, [NextChar | Tail]) ->
  case typeOfMarkupChar(NextChar) of
    whitespace -> parseAttributeValue(AttributeName, Tail);
    equalsign -> 
      {value, Value, Tail2} = parseLiteral(Tail),
      {attribute, {AttributeName, Value}, Tail2};
    _ -> 
      throw({error, "Malformed: Unexpected character in attribute value: " ++ [NextChar]})
  end;

parseAttributeValue(_, []) -> 
  throw({error, "Malformed: Unexpected end of string in attribute"}).
  
%% returns {value, Value, Tail}
parseLiteral([NextChar | Tail]) ->
  case typeOfMarkupChar(NextChar) of
    whitespace -> parseLiteral(Tail);
    quote -> parseLiteralValue(NextChar, [], Tail);
    _ -> throw({error, "Malformed: illegal value in attribute"})
  end; 
  
parseLiteral([]) ->
  throw({error, "Malformed: Unexpected end of string"}).

%% returns {Tail, State}.
parseContent(Namespaces, T = [NextChar | Tail], State, Callback) ->
  case typeOfMarkupChar(NextChar) of
    lessthan ->
      case Tail of
        [$!, $-, $- | Tail2] ->
	  {comment, Tail3} = parseComment(Tail2),
	  parseContent(Namespaces, Tail3, State, Callback);
	[$? | Tail2] ->
	  {processinginstruction, Target, Data, Tail3} = 
	     parseProcessingInstruction(Tail2),
          State2 = Callback({processingInstruction, Target, lists:reverse(Data)}, State),
	  parseContent(Namespaces, Tail3, State2, Callback);
	[$!, $[, $C, $D, $A, $T, $A, $[ | Tail2] ->
	  {cdata, CData, Tail3} = parseCDATA([], Tail2),
	  %% call callback -
	  %% If Cdata is preceded and/or followed by text there will be 2 or 3 
	  %% events, but that is legal according to the sax doc.
	  State2 = Callback({characters, CData}, State),
	  parseContent(Namespaces, Tail3, State2, Callback);
       [$/ | Tail2] ->
         %% this should be the endTag
	 {Tail2, State};
       _ ->
         {Tail2, State2} = parseElement(Namespaces, Tail, State, Callback),
	 parseContent(Namespaces, Tail2, State2, Callback)
      end;
    _ -> 
      {text, Text, IgnorableWs, Tail2} = parseText([], T),
      if 
	IgnorableWs -> State2 = Callback({ignorableWhitespace, lists:reverse(Text)}, State);
        true -> State2 = Callback({characters, lists:reverse(Text)}, State)
      end,
      parseContent(Namespaces, Tail2, State2, Callback)
  end;

parseContent(_Namespaces, [], _State, _Callback) ->
  throw({error, "Malformed: Unexpected end of string"}).

%% returns {text, Text, IgnorableWhitespace, Tail}
%% IgnorableWhitespace = true if this is only ignorable whitespace.
%% TODO: special treatment of & and CR LF

parseText(Head, Tail) ->
  case Tail of
    [$< | _] -> {text, Head, true, Tail};
    [NextChar | Tail2] -> 
      case typeOfMarkupChar(NextChar) of
        whitespace -> parseText([NextChar | Head], Tail2);
	ampersand -> 
	  {Reference, Tail3} = parseReference([], Tail2),
	  parseTextNoIgnore([Reference | Head], Tail3);
        _ -> parseTextNoIgnore([NextChar | Head], Tail2)
      end;
    [] -> throw({error, "Malformed: Unexpected end of string"})
  end.

parseTextNoIgnore(Head, "<" ++ _ = Tail) ->
    {text, Head, false, Tail};

parseTextNoIgnore(Head, "&" ++ Tail) ->
    {Reference, Tail2} = parseReference([], Tail),
    parseTextNoIgnore([Reference | Head], Tail2);

parseTextNoIgnore(Head, [NextChar| Tail]) ->
    parseTextNoIgnore([NextChar|Head], Tail);

parseTextNoIgnore(_, []) ->
    throw({error, "Malformed: Unexpected end of string"}).

parseReference(Head, Tail) ->
  case Tail of
    [$; | Tail2] ->
      Char = translateReference(lists:reverse(Head)),
      {Char, Tail2};
    [NextChar | Tail2] -> parseReference([NextChar | Head], Tail2);
    [] -> throw({error, "Malformed: Unexpected end of string in reference"})
  end.
  
% TODO: error message in case of other refernces (not supported)
translateReference(Reference) ->
  case Reference of
    "amp" -> $&;
    "lt" -> $<;
    "gt" -> $>;
    "apos" -> 39; %% apostrof
    "quot" -> 34; %% quote
    [$#, $x | Tail] -> %% hex number of char's code point
      %% unfortunately this function accepts illegal values
      %% to do: replace by something that throws an error in case of
      %% an illegal value
      httpd_util:hexlist_to_integer(Tail);
    [$# | Tail] -> %% dec number of char's code point
      case catch list_to_integer(Tail) of
        {'EXIT', _} -> throw({error, "Malformed: Illegal character in reference"});
	%% to do: check on legal character.
	Other -> Other
      end
  end.

%% returns {value, Value, Tail}
parseLiteralValue(Quote, Head, Tail) ->
  case Tail of
    [Quote | Tail2] -> {value, lists:reverse(Head), Tail2};
    %% TODO: treat & correctly
    [$& | Tail2] -> parseLiteralValue(Quote, [$& | Head], Tail2); 
    [NextChar | Tail2] -> parseLiteralValue(Quote, [NextChar | Head], Tail2);
    [] -> throw({error, "Malformed: Unexpected end of string in literal value"})
  end.

%% </ have already been taken off
parseEndTag(">" ++ Tail, ResultSoFar) ->
    {endtag, lists:reverse(ResultSoFar), Tail};

parseEndTag([NextChar | Tail], ResultSoFar) ->
    parseEndTag(Tail, [NextChar | ResultSoFar]);

parseEndTag([], _) ->
  throw({error, "Malformed: Unexpected end of string in end tag"}).

%% StartTag = {Prefix, LocalName, QualifiedName}
%% Attributes = list of Attribute
%% Attribute = {{Prefix, LocalName} Value}
%%
%% returns: {Name, Attributes2, NewNamespaces}
%% Name = {URI, LocalName, QualifiedName}
%% Attributes2 = list of Attribute2
%% Atrribute2 = #attribute
%% NewNamespaces = list of {Prefix, URI} (prefix can be []).
%%
%% Namespaces are in such an order that namespace of the 'closest ancestors' 
%% are in front. That way the right element will be found, even if a prefix is 
%% used more than once in the document.
%%
createStartTagEvent(StartTag, Namespaces, Attributes) ->
  
  %% find the namespace definitions in the attributes
  {NewNamespaces, OtherAttributes} = lookForNamespaces([], [], Attributes),
  AllNamespaces = NewNamespaces ++ Namespaces,

  %% add the Uri to the tag name (if applicable)
  Name = tagNameTuple(StartTag, AllNamespaces),

  %% add the URIs to the attribute names (if applicable)
  Attributes2 = attributeNameTuples([], OtherAttributes, AllNamespaces),

  {Name, Attributes2, NewNamespaces}.

%% returns {Namespaces, OtherAttributes}, where 
%%   Namespaces = a list of tuples {Prefix, URI} 
%%   OtherAttributes = a list of tuples {Name, Value}
%%
lookForNamespaces(Namespaces, OtherAttributes, [Head | Tail]) ->
  {{Prefix, LocalName, _QName}, Value} = Head,
  if 
    Prefix == "xmlns" ->
      lookForNamespaces([{LocalName, Value} | Namespaces], 
                         OtherAttributes, Tail);
    Prefix == [],  LocalName == "xmlns" ->
      lookForNamespaces([{[], Value} | Namespaces], 
                        OtherAttributes, Tail);
    true -> 
      lookForNamespaces(Namespaces, [Head | OtherAttributes], Tail)
  end;
  
lookForNamespaces(Namespaces, OtherAttributes, []) -> 
  {Namespaces, OtherAttributes}.

%% StartTag = {Prefix, LocalName, QualifiedName} 
%% Namespaces = list of {Prefix, URI} (prefix can be []).
%%
%% Returns {Uri, LocalName, Prefix}
%%
%% TODO: error if not found? special treatment of 'xml:lang'?
tagNameTuple(StartTag, Namespaces) ->
  {Prefix, LocalName, _QName} = StartTag,
  case lists:keysearch(Prefix, 1, Namespaces) of
    {value, {Prefix, Uri}} -> {Uri, LocalName, Prefix};
    false -> {[], LocalName, Prefix}
  end.
      

%% Attributes = list of Attribute
%% Attribute = {{Prefix, LocalName} Value}
%% Namespaces = list of {Prefix, URI} (prefix can be []).
%%
%% Returns a list of #attribute records
attributeNameTuples(ProcessedAttributes, 
                    [{AttributeName, Value} | Attributes], Namespaces) ->
  {Uri, LocalName, Prefix} = attributeNameTuple(AttributeName, Namespaces),
  attributeNameTuples([#attribute{localName= LocalName,
                                  prefix = Prefix,
				  uri = Uri,
				  value = Value} | ProcessedAttributes], 
                      Attributes, Namespaces);

attributeNameTuples(ProcessedAttributes, [], _) ->
  ProcessedAttributes.

%% AttributeName = {Prefix, LocalName, QualifiedName}
%% Namespaces = list of {Prefix, URI} (prefix can be []).
%%
%% Returns {Uri, LocalName, Prefix}.
%% Difference with TagNameTuple: attributes without prefix do NOT belong
%% to the default namespace.
attributeNameTuple(AttributeName, Namespaces) ->
  {Prefix, LocalName, _} = AttributeName,
  if 
    Prefix == [] -> {[], LocalName, LocalName};
    true -> 
      case lists:keysearch(Prefix, 1, Namespaces) of
        {value, {Prefix, Uri}} ->
	    {Uri, LocalName, Prefix};
        false ->
            case Prefix of
              "xml" -> {"http://www.w3.org/XML/1998/namespace", LocalName, Prefix};
              _ -> {[], LocalName, Prefix}
            end
      end
  end.

%% Context can be:
%%  name, markup
%% Returns:
%%  whitespace, lessthan, morethan, etc, see below.
typeOfNameChar(Char) ->
  if 
    Char > 96 ->
      if 
        Char < 123 -> namechar;
	Char < 127 -> char;
	true -> illegal
      end;
    Char > 64 ->
      if 
        Char < 91 -> namechar;
        Char == 95 -> namechar;
	true -> char
      end;
    Char > 47 ->
      if
        Char < 59 -> namechar;
	Char == 61 -> equalsign;
	Char == 62 -> morethan;
	Char == 63 -> questionmark;
	true -> char
      end;
    Char > 32 ->
      if 
        Char < 40 -> char;
        Char < 45 -> illegal;
	Char == 47 -> slash;
	true -> namechar
      end;
    Char == 32 -> whitespace;
    Char == 9 -> whitespace;
    Char == 10 -> whitespace;
    Char == 13 -> whitespace;
    true -> illegal
  end.


%% Context can be:
%%  name, markup
%% Returns:
%%  whitespace, lessthan, morethan, etc, see below.
typeOfMarkupChar(Char) ->
  if 
    Char == 32 -> whitespace;
    Char > 96 ->
      if 
        Char < 123 -> namestartchar;
	Char < 127 -> char;
	true -> illegal
      end;
    Char > 64 ->
      if 
        Char < 91 -> namestartchar;
        Char == 95 -> namestartchar;
	true -> char
      end;
    Char > 40 ->
      if
        Char == 47 -> slash;
        Char < 60 -> char;
	Char == 60 -> lessthan;
	Char == 61 -> equalsign;
	Char == 62 -> morethan;
	true -> char
      end;
    Char > 32 ->
      if 
        Char == 39 -> quote;
        Char == 34 -> quote;
        Char == 38 -> ampersand;
	true -> char
      end;
    Char == 9 -> whitespace;
    Char == 10 -> whitespace;
    Char == 13 -> whitespace;
    true -> illegal
  end.
